Garbage First收集器
Garbage First 收集器简称G1
,是垃圾收集器技术发展史上的里程碑式的成果,
开创了收集器面向局部收集的设计思路和基于Region
的内存布局形式。
G1
是一款主要面向服务端应用的垃圾收集器,被赋予替代CMS
的期望。
JDK9
发布后,G1
宣告取代了Parallel Scavenge + Parallel Old
组合,
成为收集器服务端模式下的默认垃圾收集器,而CMS
则被声明为不推荐使用(Deprecate
)。
特色
停顿时间模型(
Pause Prediction Model
):意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒这样的目标。
设计者们希望G1
是一款能够建立停顿时间模型的收集器,
以往的收集器垃圾收集的范围要么是整个新生代(Minor GC
),要么是整个老年代(Major GC
),
要么是整个堆(Full GC
)。而G1
不是,它可以面向堆内存任何部分来组成回收集(Collection Set
,简称CSet
)
进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,
这就是G1
收集器的Mixed GC
模式。
基于Region
的堆内存布局
G1
开创的基于Region
的堆内存布局是G1
能建立停顿时间模型的关键。
G1
也仍然遵循分代收集理论设计,但不再坚持固定大小以及固定数量的分代区域划分,
而是把连续的Java
堆划分为多个大小相等的独立区域(Region
),每一个Region
都可以
根据需要扮演新生代的Eden
空间、Survivor
空间,或者是老年代空间。
收集器能够对扮演不同角色的Region
采用不同的策略去处理。
Region
中还有一类特殊的Humongous
区域,专门用来存储大对象。G1
认为大小超过一个Region
容量一半的对象即可判定为大对象。每个Region
的大小可以通过-XX:G1HeapRegionSize
设定,
取值范围为1MB~32MB
,且应为2的N
次幂。对于那些超过整个Region
容量的超级大对象,将会
存放在N
个连续的Humongous Region
之中,G1
的大多数行为都把Humongous Region
作为老年代
的一部分进行看待。
能建立可预测的停顿时间模式的原因
是因为G1
将Region
作为单次回收的最小单元,即每次收集到的内存空间都是Region
大小的整数倍,
这样可以有计划地避免在整个Java
堆中进行全区域的垃圾收集。
具体思路就是让G1
收集器跟踪各个Region
里边的垃圾堆积的“价值”大小,价值即回收所获得的空间大小
以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间
(使用-XX:MaxGCPauseMillis
指定,默认是200毫秒),优先处理回收价值收益最大的那些Region
,
这也是Garbage First
名字的由来。
是有标记过吗,不然怎么知道回收的价值?
其实是有统计数据的,见下边
G1
实现难点及方案-> 如何建立可靠的停顿预测模型
G1
实现难点及方案
跨代引用问题的解决
将Java
堆分成多个独立的Region
后,Region
里边存在跨Region
引用对象同样也需要使用记忆集来避免
全堆作为GC Root
扫描,G1
的记忆集复杂得多,每个Region
都维护自己的记忆集,这些记忆集会记录
别的Region
指向自己的指针,并标记这些指针分别在哪些卡页的范围内。
G1
的记忆集在存储结构的本质上是一种哈希表,key
是别的Region
的起始地址,value
是一个集合,
存储的元素是卡表的索引号。这种双向的卡表结构(卡表是“我指向谁”,这种结构还记录了谁指向我)
比原来的卡表实现更复杂,同时由于Region
数量比传统收集器的分代数量明显要多得多,因此G1
收集器
要比其他传统垃圾收集器有着更高的内存中用负担。至少要耗费相当于Java
堆容量10%至20%的额外内存来维持工作。
每个
Region
都维护自己的记忆集是怎么理解?就是每个
Region
都有自己的记忆集。并发标记时需要遍历所有
Region
的记忆集吗?不用的,要记住
G1
不是整个新生代or老年代回收的,是选择性地回收Region
(回收集),所以只遍历要回收的回收集的
Region
的记忆集即可。之前是只有年轻代建立了卡表,用于标记老年代哪块内存中存在指向新生代的应用。现在是新生代和老年代都有了吗?
是的呀
双向要怎么理解?这里的我是代表啥?是
Region
吗?原来的卡表表达的意思就是某个内存中存在跨代引用的指针,即我指向谁。
对于一个
Region
来说,key是别的Region
的起始地址,即谁指向了我(哪个内存中有对象引用了我这个Region中的对象)。
value
是卡表的索引值(最终要靠卡表找到卡页,才能找到需要扫描的内存)
并发标记的问题处理
CMS
收集器采用增量更新的方式实现,而G1
是使用原始快照(SATB
)算法来实现。
垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要
继续运行肯定会持续有新对象被创建,G1
为每一个Region
设计了两个名为TAMS
(Top at Mark Start
)的指针,把Region
的一部分空间划分出来用于并发回收
过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
啥?
如何建立可靠的停顿预测模型
用户通过-XX:MaxGCPauseMillis
参数指定的停顿时间只意味着垃圾收集发生之前的期望值,
但如何才能满足用户的期望呢?G1
收集器的停顿预测模式是以衰减均值(Decaying Average
)
为理论基础来实现的,在垃圾收集过程中,G1
收集器会记录每个Region
的回收耗时、每个
Region
记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、
置信度等统计信息。
这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体
平均状态,但衰减平均值更准确地代表“最近的”平均状态。也就是Region
的统计状态越新,
越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region
组成回收集
才可以在不超过期望停顿时间的约束下获得最高的收益。
G1收集器回收过程
不计算用户线程运行过程中的动作(如写屏障维护记忆集)
初始标记(
Initial Marking
)stw
仅仅知识标记一下
GC Roots
能关联到的对象,并修改TAMS
指针的值,让下一阶段用户线程并发运行时,能正确地在可用
Region
空间中分配新对象。这个阶段需要停顿线程(根节点枚举都是要STW
的),但耗时较短,而且是借助进行
Minor GC
的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。并发标记(
Concurrent Marking
)从
GC Roots
开始对堆中对象进行可达性分析,递归扫描整个堆的对象图,此阶段耗时较长,但是并发的。扫描完成后,重新处理
SATB
记录下的在并发时有引用变动的对象。(原始快照)最终标记(
Final Marking
)stw
对用户线程做另一个短暂停顿,用于处理并发阶段结束后仍遗留下来的最后那少量的
SATB
记录。筛选回收(
Live Data Counting and Evacuation
)stw
负责更新
Region
的统计数据,对各个Region
的回收价值和成本进行排序,根据用户所期盼的停顿时间来制定回收计划,可以自由选择多个
Region
构成回收集,然后把决定回收的那一部分Region
的存活对象复制到空的Region
中,在清理掉整个旧的Region
的全部空间。这里的操作设计存活对象的移动,是必须暂停用户线程的,由多条收集器线程并行完成。
前三个步骤还蛮像
CMS
的,CMS
第四个是并发清理。
G1
收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,并非纯粹地追求低延迟,
官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
可以由用户指定期望停顿时间是G1
收集器很强大的一个功能。默认的停顿时间为两百毫秒,
一般来说回收阶段占几十到一百甚至接近两百毫秒都很正常。
G1 与 CMS
相比CMS
,G1
的优点有很多
可以指定最大停顿时间
分
Region
的内存布局、按照收益动态确定回收集带来的好处G1
从整体来看是基于“标记-整理”算法实现的收集器,但从局部(Region
之间)又是“标记-复制”算法。意味着
G1
运作期间不会产生内存空间碎片。
CMS
的胜出点
G1
无论是为了垃圾收集产生的内存占用(Footprint
)还是程序运行时的额外执行负载(Overload
)都要比
CMS
要高。G1
和CMS
都使用卡表来处理跨代指针,但G1
的卡表实现更为复杂,而且无论扮演的是新生代还是老年代,都必须有一份卡表,这导致
G1
的记忆集(和其他内存消耗)可能占整个堆容量20%乃至更多内存空间;CMS
的卡表相当简单,只有一份,而且只需要处理老年代到新生代的引用,反过来不需要。(代价就是当CMS
发生Old GC
时,要把整个新生代作为GC Roots
来进行扫描)
选择
在小内存应用上CMS
的表现大概率要优于G1
,而在大内存应用上G1
则能发挥其优势。
这个优劣势的Java
堆容量平衡点通常在6GB
至8GB
。